package com.googlecode.cswish.struts;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.commons.lang.ClassUtils;
import org.apache.log4j.Logger;
import org.apache.struts2.StrutsConstants;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.struts.conversion.StrutsFreemarkerManager;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.StrutsObjectFactory;
import com.googlecode.cswish.util.bean.ModelUtil;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.UnknownHandler;
import com.opensymphony.xwork2.XWorkException;
import com.opensymphony.xwork2.config.Configuration;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.config.entities.ExceptionMappingConfig;
import com.opensymphony.xwork2.config.entities.InterceptorMapping;
import com.opensymphony.xwork2.config.entities.PackageConfig;
import com.opensymphony.xwork2.config.entities.ResultConfig;
import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
import com.opensymphony.xwork2.config.providers.InterceptorBuilder;
import com.opensymphony.xwork2.inject.Inject;

/**
 * Create Action configuration and Result configuration
 * 
 * @author Jerry.Feng
 */
public class CSwishUnknownHandler implements UnknownHandler {

	private static final Logger logger = Logger
			.getLogger(CSwishUnknownHandler.class);

	protected boolean devMode;

	protected String defaultPackageName;
	
	protected Configuration configuration;

	protected StrutsObjectFactory objectFactory;
	
	private BeanIntrospector beanIntrospector;

	private Routing routing;
	
	private Config config;
	
	private StrutsFreemarkerManager strutsFreemarkerManager;
	
	protected LinkedHashMap<String, ResultTypeConfig> resultsByExtension;
	
	private Map<String, ResultConfig> commonResults = new HashMap<String, ResultConfig>();
	
	@Inject
	public CSwishUnknownHandler(@Inject Configuration configuration, 
			@Inject("struts.cswish.defaultPackage") String defaultPackageName,
			@Inject(StrutsConstants.STRUTS_ACTION_EXTENSION) String actionExtension) {
		this.configuration = configuration;
		this.defaultPackageName = defaultPackageName;
		
        resultsByExtension = new LinkedHashMap<String,ResultTypeConfig>();
        PackageConfig parentPackage = configuration.getPackageConfig(defaultPackageName);
        if (parentPackage == null) {
            throw new ConfigurationException("Unknown parent package: " + parentPackage);
        }    
        Map<String,ResultTypeConfig> results = parentPackage.getAllResultTypeConfigs();
        
        // template list
        resultsByExtension.put("ftl", results.get("freemarker"));
        resultsByExtension.put("html", results.get("freemarker"));
        resultsByExtension.put("jsp", results.get("dispatcher"));
        //resultsByExtension.put("vm", results.get("velocity"));
        //resultsByExtension.put("htm", results.get("freemarker"));
	}

	
	@Inject(StrutsConstants.STRUTS_DEVMODE)
    public void setDevMode(String mode) {
        this.devMode = "true".equals(mode);
        
        if (config != null) {
        	config.setDevMode(devMode);
        }
    }
	
	@Inject
	public void setObjectFactory(com.opensymphony.xwork2.ObjectFactory objectFactory) {
		this.objectFactory = (StrutsObjectFactory)objectFactory;
		
		routing = this.objectFactory.getBean(Routing.class, false);
		config = this.objectFactory.getBean(Config.class, false);
		config.setDevMode(devMode);
		strutsFreemarkerManager = this.objectFactory.getBean(StrutsFreemarkerManager.class, false);
		beanIntrospector = this.objectFactory.getBean(BeanIntrospector.class, false);
	}

	@Override
	public ActionConfig handleUnknownAction(String namespace, String context)
			throws XWorkException {
		ActionConfig cfg = buildActionConfig(namespace, context, true);
		return cfg;
	}
	
	public ActionConfig buildActionConfig(String namespace, String context, boolean buildMatched) {
		PackageConfig packageConfig = configuration.getPackageConfig(defaultPackageName);

		Invoker invoker = routing.path2Invoker(context, true);
		
		// results
		Map<String, ResultConfig> defaultResults = packageConfig.getAllGlobalResults();
		Map<String, ResultConfig> results = new HashMap<String, ResultConfig>();
		results.putAll(defaultResults);
		
		// we need check the path dynamically in develop mode, so the result 
		// is created in 'handleUnknownResult'
		if (!devMode) {
			ResultConfig success = buildResult(Action.SUCCESS, context);
			results.put(Action.SUCCESS, success);
			// input page definition rule
			if (!invoker.isInputMethod() && !"execute".equals(invoker.methodName)) {
				Invoker inputInvoker = new Invoker(invoker.actionName, invoker.separator,
						invoker.getInputMethod(), invoker.siteName);
				String inputContext = routing.invoker2PathNoExtension(inputInvoker, true);
				ResultConfig input = buildResult(Action.INPUT, inputContext);
				results.put(Action.INPUT, input);
			}
			
			// the common flag
			results.put("$element", getCommonResult("$element"));
			results.put("$close", getCommonResult("$close"));
			results.put("$json", getCommonResult("$json"));
			results.put("$noResult", getCommonResult("$noResult"));
		}

		// parameter
		Map<String, String> params = new TreeMap<String, String>();

		// interceptor
		String defaultInterceptorRefName = packageConfig
				.getDefaultInterceptorRef();
		List<InterceptorMapping> interceptors = new ArrayList<InterceptorMapping>();
		if (defaultInterceptorRefName != null) {
			interceptors.addAll(InterceptorBuilder
					.constructInterceptorReference(packageConfig,
							defaultInterceptorRefName, new LinkedHashMap(),
							packageConfig.getLocation(), objectFactory));
		}

		// exception
		List<ExceptionMappingConfig> exceptionMappings = packageConfig.getAllExceptionMappingConfigs();
		
		// package name
		String packageName = defaultPackageName;
		
		// configuration
		String className;
		try {
			ClassUtils.getClass(invoker.actionName);
			className = invoker.actionName;
		} catch (ClassNotFoundException ex) {
			className = config.getBaseAction().getName();
		}
		
		//objectFactory.registerAction(className);
		ActionConfig.Builder builder = new ActionConfig.Builder(packageName, null, className);
		builder.addResultConfigs(results);
		builder.addParams(params);
		builder.interceptors(interceptors);
		builder.exceptionMappings(exceptionMappings);
		ActionConfig config = builder.build();
		config.setLocation(packageConfig.getLocation());
		// FIXME: Out of Memory if there's no security filter
		Map<String, Map<String, ActionConfig>> actionConfigs = configuration.getRuntimeConfiguration().getActionConfigs();
		actionConfigs.get("").put(context, config);
		
		if (buildMatched && !"execute".equals(invoker.methodName)) {
			String matchedMethod;
			if (invoker.isInputMethod()) {
				matchedMethod = invoker.getSubmitMethod();
			} else {
				matchedMethod = invoker.getInputMethod();
			}
			String simpleName = beanIntrospector.getSimpleName(invoker.actionName);
			String newContext = Routing.invoker2PathNoExtension(simpleName,
					invoker.separator, matchedMethod, invoker.siteName);
			if (!packageConfig.getActionConfigs().containsKey(newContext)) {
				buildActionConfig(namespace, newContext, false);
			}
		}
		
		return config;
	}
	
	private ResultConfig getCommonResult(String resultCode) {
		ResultConfig resultConfig = commonResults.get(resultCode);
		
		if (resultConfig == null) {
			HashMap<String, String> resultParams = new HashMap<String, String>();
			String location = config.getCommonViewPkg() + "generic." + resultCode + ".ftl";
			resultParams.put("location", location);
			String resultClassName = resultsByExtension.get("ftl").getClassName();
			ResultConfig.Builder builder = new ResultConfig.Builder(resultCode, resultClassName);
			builder.addParams(resultParams);
			resultConfig = builder.build();
			
			commonResults.put(resultCode, resultConfig);
		}
		return resultConfig;
	}

	private ResultConfig buildResult(String resultCode, String context) {	
		HashMap<String, String> resultParams = new HashMap<String, String>();
		String location = findLocation(resultCode, context);
		String extension = location.substring(location.lastIndexOf('.') + 1);
		String resultClassName = resultsByExtension.get(extension).getClassName();
		if (logger.isDebugEnabled()) {
			logger.debug("Result code: " + resultCode + ", Forward to Page: " + location);
		}
		resultParams.put("location", location);
		ResultConfig.Builder builder = new ResultConfig.Builder(resultCode, resultClassName);
		builder.addParams(resultParams);
		ResultConfig success = builder.build();
		return success;
	}

	protected String findLocation(String resultCode, String context) {
		// use the method name
		Invoker invoker = routing.path2Invoker(context, true);
		if (!resultCode.equals(Action.INPUT) && !resultCode.equals(Action.SUCCESS)) {
			invoker.methodName = resultCode;
		}
		
		String location = strutsFreemarkerManager.findLocation(context, invoker);
		return location;
	}
	
	@Override
	public Result handleUnknownResult(ActionContext actionContext,
			String actionName, ActionConfig actionConfig, String resultCode)
			throws XWorkException {
		String context = actionName;
		if (Action.INPUT.equals(resultCode)) {
			Invoker invoker = routing.path2Invoker(context, true);
			if (!invoker.isInputMethod()) {
				invoker.methodName = invoker.getInputMethod();
				context = routing.invoker2PathNoExtension(invoker, true);
			}
		}
		ResultConfig resultConfig = buildResult(resultCode, context);
		
		if (!devMode) {
			// Add resultConfig to actionConfig
			Map<String, ResultConfig> results = actionConfig.getResults();
			ConcurrentHashMap<String, ResultConfig> map;
			if (results instanceof ConcurrentHashMap) {
				map = (ConcurrentHashMap<String, ResultConfig>) results;
			} else {
				map = new ConcurrentHashMap<String, ResultConfig>(results);
				ModelUtil.setField(actionConfig, "results", map);
			}
			map.put(resultCode, resultConfig);
		}
		
		try {
			return objectFactory.buildResult(resultConfig, actionContext.getContextMap());
		} catch (Exception e) {
            throw new XWorkException("Unable to build cwish result", e, resultConfig);
        }
	}

	@Override
	public Object handleUnknownActionMethod(Object action, String methodName)
			throws NoSuchMethodException {
		return null;
	}
}